/*
 * Class
 * DiscreteDistribution
 */
package pl.abstractvoid.distributions;

import static java.lang.StrictMath.min;
import java.util.ArrayList;
import java.util.Random;
import java.util.TreeMap;
import pl.abstractvoid.datamodel.parameters.ParameterSet;
import pl.abstractvoid.datamodel.parameters.ParameterTable;
import pl.abstractvoid.datamodel.parameters.SingleParameter;
import pl.abstractvoid.datamodel.parameters.exceptions.NoSuchParameterException;
import pl.abstractvoid.distributions.exceptions.ParameterValidationFailException;
import pl.abstractvoid.rconnector.RCallerInstance;
import pl.abstractvoid.rconnector.RInterpreter;
import rcaller.exception.ParseException;

/**
 * Represents a discrete probability distribution with finite number of values.
 *
 * @author Wojciech Szałapski
 */
public class DiscreteDistribution extends AbstractDistribution {

    /**
     * String representing the name of the table of distribution's parameters
     * for R calls.
     */
    private final static String randomVariableTableString = "randomVariableValuesAndProbabilities";
    /**
     * String representing the name of the table of distribution's parameters
     * visible in user interface.
     */
    private final static String visibleRandomVariableTableString = "Rozkład zmiennej losowej";
    /**
     * String representing the name of the vector of random variable's values
     * for R calls.
     */
    private final static String randomVariableValuesString = "randomVariable";
    /**
     * String representing the name of the vector of random variable value's
     * probabilities for R calls.
     */
    private final static String randomVariableProbabilitiesString = "randomVariableProbs";
    /**
     * Names for output values used by R interpreter.
     */
    private final static String[] outputTableNames;
    /**
     * Names for output values used by user interface.
     */
    private final static String[] outputTableVisibleNames;
    /**
     * Buffer for the script executed when the output is computed.
     */
    private final static StringBuilder discreteSetUpOutputScript = new StringBuilder();
    /**
     * Buffer for the script executed when the cumulative distribution plot is
     * generated.
     */
    private final static StringBuilder discreteSetUpCumulativeDistributionPlotScript = new StringBuilder();
    /**
     * Buffer for the script executed when the probability density/mass function 
     * plot is generated.
     */
    private final static StringBuilder discreteSetUpProbabilityPlotScript = new StringBuilder();

    /*
     * Initialization of:
     *  - Names of output parameters.
     *  - Script for computing output parameters.
     *  - Scripts for generating plots.
     */
    static {
        final int numberOfParameters = 3;
        outputTableNames = new String[numberOfParameters];
        outputTableVisibleNames = new String[numberOfParameters];
        outputTableNames[0] = "expectedValue";
        outputTableNames[1] = "variance";
        outputTableNames[2] = "standardDeviation";

        discreteSetUpOutputScript.append(outputTableNames[0]).append(" = sum(").
                append(RInterpreter.inputRList).append("$").append(randomVariableValuesString).append(" * ").
                append(RInterpreter.inputRList).append("$").append(randomVariableProbabilitiesString).append(")\n");
        discreteSetUpOutputScript.append(outputTableNames[1]).append(" = sum(").
                append(RInterpreter.inputRList).append("$").append(randomVariableValuesString).append(" * ").
                append(RInterpreter.inputRList).append("$").append(randomVariableValuesString).append(" * ").
                append(RInterpreter.inputRList).append("$").append(randomVariableProbabilitiesString).append(") - ").
                append(outputTableNames[0]).append("^2\n");
        discreteSetUpOutputScript.append(outputTableNames[2]).append(" = ").
                append(outputTableNames[1]).append("^(1/2)\n");
        discreteSetUpOutputScript.append(RInterpreter.outputRList).append(" = list(");
        String prefix = "";
        for (String name : outputTableNames) {
            discreteSetUpOutputScript.append(prefix).append(name).append(" = ").append(name);
            prefix = ", ";
        }
        discreteSetUpOutputScript.append(")");

        // TODO: make generating vectors more efficient by appropriate sorting
        final int plotPrecision = 5000;
        discreteSetUpCumulativeDistributionPlotScript.append(
            "randomVariableMinimumValue = min(").append(
            RInterpreter.inputRList).append("$").append(randomVariableValuesString).
            append(")\n").append(
            "randomVariableMaximumValue = max(").append(
            RInterpreter.inputRList).append("$").append(randomVariableValuesString).
            append(")\n").append(
            "plotRange = randomVariableMaximumValue - randomVariableMinimumValue\n"
            + "plotX = seq(randomVariableMinimumValue - 0.1 * plotRange, randomVariableMaximumValue + 0.1 * plotRange, "
            + "length.out = ").append(plotPrecision).append(")\n"
            + "plotY = plotX - plotX\n"
            + "for (i in c(1:length(").append(
            RInterpreter.inputRList).append("$").append(randomVariableValuesString).
            append("))) {\n"
            + " plotIterator = length(plotX)\n"
            + " plotCurrentProb = ").append(
            RInterpreter.inputRList).append("$").append(randomVariableProbabilitiesString).
            append("[i]\n").append(
            " while (plotIterator > 0 && plotX[plotIterator] >= ").append(
            RInterpreter.inputRList).append("$").append(randomVariableValuesString).
            append("[i]) {\n"
            + "  plotY[plotIterator] = plotY[plotIterator] + plotCurrentProb\n"
            + "  plotIterator = plotIterator - 1\n"
            + " }\n"
            + "}\n"
            + "normalPlot(plotX, plotY, type = 'l')");
        
        discreteSetUpProbabilityPlotScript.append(
            "plotX = unique(").append(RInterpreter.inputRList).append("$").append(
            randomVariableValuesString).append(")\n").append(
            "plotY = plotX - plotX\n" + 
            "for (i in c(1:length(plotX))) {\n" + 
            " for (j in c(1:length(").append(
            RInterpreter.inputRList).append("$").append(randomVariableValuesString).append(
            "))) {\n" + 
            "  if (plotX[i] == ").append(
            RInterpreter.inputRList).append("$").append(randomVariableValuesString).append(
            "[j]) {\n" + 
            "   plotY[i] = plotY[i] + ").append(
            RInterpreter.inputRList).append("$").append(randomVariableProbabilitiesString).append(
            "[j]\n" + 
            "  }\n" + 
            " }\n" + 
            "}\n" + 
            "normalPlot(plotX, plotY, type = 'p')");
    }

    /**
     * Constructs the distribution using given parameters and names.
     *
     * @param randomVariableValues Values of the random variable.
     * @param randomVariableProbabilities Probabilities of consecutive values of
     * the random variable.
     * @throws ParameterValidationFailException
     * @throws NoSuchParameterException
     */
    public DiscreteDistribution(ArrayList<Double> randomVariableValues,
            ArrayList<Double> randomVariableProbabilities)
            throws ParameterValidationFailException, NoSuchParameterException {
        super(discreteSetUpOutputScript, discreteSetUpCumulativeDistributionPlotScript, 
              discreteSetUpProbabilityPlotScript);
        distributionName = "discreteDistribution";
        rState = new RCallerInstance();
        ParameterTable randomVariableTable = new ParameterTable(randomVariableTableString,
                visibleRandomVariableTableString,
                new String[]{randomVariableValuesString, randomVariableProbabilitiesString});
        randomVariableTable.setParameterValues(0, randomVariableValues);
        randomVariableTable.setParameterValues(1, randomVariableProbabilities);
        TreeMap<String, ParameterTable> parameterTables = new TreeMap<>();
        parameterTables.put(randomVariableTableString, randomVariableTable);
        ParameterSet parameterSet = new ParameterSet();
        parameterSet.setParameterTables(parameterTables);
        updateInput(parameterSet);
    }

    @Override
    protected final void validate(ParameterSet parameters) throws ParameterValidationFailException {
        TreeMap<String, ParameterTable> parameterTables = parameters.getParameterTables();
        if (!parameterTables.containsKey(randomVariableTableString)) {
            throw new ParameterValidationFailException("randomVariableTableNotFound");
        } else {
            ParameterTable parameterTable = parameterTables.get(randomVariableTableString);
            ArrayList<ArrayList<Double>> parameterValues = parameterTable.getParameterValues();
            ArrayList<Double> randomVariableVector = parameterValues.get(0);
            ArrayList<Double> randomVariableProbsVector = parameterValues.get(1);
            if (randomVariableVector.size() != randomVariableProbsVector.size()) {
                throw new ParameterValidationFailException("invalidRandomVariableVectors");
            } else {
                final double tolerance = 1e-4;
                double overallProbability = 0;
                for (Double p : randomVariableProbsVector) {
                    if (p < 0 || p > 1) {
                        throw new ParameterValidationFailException("invalidProbabilityValue");
                    }
                    overallProbability += p;
                }
                if (overallProbability < 1 - tolerance || overallProbability > 1 + tolerance) {
                    throw new ParameterValidationFailException("invalidProbabilitySum");
                }
            }
        }
    }

    @Override
    public ParameterSet loadParameters() throws NoSuchParameterException, ParseException {
        rState.runCustomCode("", RInterpreter.inputRList);
        ArrayList<Double> randomVariableVector = getParameterVectorValue(randomVariableValuesString);
        ArrayList<Double> randomVariableProbsVector = getParameterVectorValue(randomVariableProbabilitiesString);
        final int minSize = min(randomVariableVector.size(), randomVariableProbsVector.size());
        for (int i = randomVariableVector.size() - 1; i >= minSize; --i) {
            randomVariableVector.remove(i);
        }
        for (int i = randomVariableProbsVector.size() - 1; i >= minSize; --i) {
            randomVariableProbsVector.remove(i);
        }
        ParameterTable randomVariableTable = new ParameterTable(randomVariableTableString,
                visibleRandomVariableTableString,
                new String[]{randomVariableValuesString, randomVariableProbabilitiesString});
        randomVariableTable.setParameterValues(0, randomVariableVector);
        randomVariableTable.setParameterValues(1, randomVariableProbsVector);
        TreeMap<String, ParameterTable> parameterTables = new TreeMap<>();
        parameterTables.put(randomVariableTable.getTableName(), randomVariableTable);
        ParameterSet result = new ParameterSet();
        result.setParameterTables(parameterTables);
        return result;
    }

    @Override
    protected void initializeOutputTable() {
        outputTable = new SingleParameter[outputTableNames.length];
        for (int i = 0; i < outputTableNames.length; ++i) {
            outputTable[i] = new SingleParameter(outputTableNames[i]);
        }
    }

    /**
     * Returns a new instance of a discrete distribution with random input
     * parameter set.
     *
     * @return New instance of discrete distribution with random parameters.
     */
    public static DiscreteDistribution getSampleDistributionData() {
        ArrayList<Double> randomVariableValues = new ArrayList<>();
        ArrayList<Double> randomVariableProbabilities = new ArrayList<>();
        double probSum = 0;
        for (int i = 0; i < 10; ++i) {
            randomVariableValues.add((double) i);
            randomVariableProbabilities.add(new Random().nextDouble() * (1 - probSum));
            if (i < 9) {
                probSum += randomVariableProbabilities.get(i);
            }
        }
        randomVariableProbabilities.set(9, 1 - probSum);
        try {
            return new DiscreteDistribution(randomVariableValues,
                    randomVariableProbabilities);
        } catch (ParameterValidationFailException | NoSuchParameterException ex) {
            return null;
        }
    }
}
